//
// Created by kode.zhong on 2021/3/14.
//

#include "scanner.h"

#include <utility>

namespace lox {
Scanner::Scanner(const std::string& source) : m_source{source},
                                       m_start{m_source.begin()}, m_current{m_source.begin()}, m_line{1} {
}

bool IsDigit(char c) {
  return c >= '0' && c <= '9';
}

bool IsAlpha(char c) {
  return (c >= 'a' && c <= 'z') ||
      (c >= 'A' && c <= 'Z') ||
      c == '_';
}

Token Scanner::ScanToken() {
  SkipWhitSpace();
  m_start = m_current;
  if (IsAtEnd()) return MakeToken(TOKEN_EOF);

  char c = Advance();
  if (IsAlpha(c)) return MakeIdentifier();
  if (IsDigit(c)) return MakeNumber();

  switch (c) {
    case '(': return MakeToken(TOKEN_LEFT_PAREN);
    case ')': return MakeToken(TOKEN_RIGHT_PAREN);
    case '{': return MakeToken(TOKEN_LEFT_BRACE);
    case '}': return MakeToken(TOKEN_RIGHT_BRACE);
    case ';': return MakeToken(TOKEN_SEMICOLON);
    case ',': return MakeToken(TOKEN_COMMA);
    case '.': return MakeToken(TOKEN_DOT);
    case '-': return MakeToken(TOKEN_MINUS);
    case '+': return MakeToken(TOKEN_PLUS);
    case '/': return MakeToken(TOKEN_SLASH);
    case '*': return MakeToken(TOKEN_STAR);
    case '!':return MakeToken(Match('=') ? TOKEN_BANG_EQUAL : TOKEN_BANG);
    case '=':return MakeToken(Match('=') ? TOKEN_EQUAL_EQUAL : TOKEN_EQUAL);
    case '<':return MakeToken(Match('=') ? TOKEN_LESS_EQUAL : TOKEN_LESS);
    case '>':return MakeToken(Match('=') ? TOKEN_GREATER_EQUAL : TOKEN_GREATER);
    case '"': return MakeString();
    default:
      return ErrorToken("Unexpected character.");
  }
}

bool Scanner::IsAtEnd() const {
  return m_current == m_source.cend();
}

Token Scanner::MakeToken(TokenType type) const {
  Token token;
  token.type = type;
  token.lexme = std::string(m_start, m_current);
  token.line = m_line;
  return token;
}

Token Scanner::ErrorToken(const char *msg) const {
  Token token;
  token.type = TOKEN_ERROR;
  token.lexme = msg;
  token.line = m_line;
  return token;
}

char Scanner::Advance() {
  ++m_current;
  return m_current[-1];
}

bool Scanner::Match(char expected) {
  if (IsAtEnd()) return false;
  if (*m_current != expected) return false;
  ++m_current;
  return true;
}

void Scanner::SkipWhitSpace() {
  for (;;) {
    char c = Peek();
    switch (c) {
      case ' ':
      case '\r':
      case '\t':Advance();
        break;
      case '\n':++m_line;
        Advance();
        break;
      case '/':
        if (PeekNext() == '/') {
          // Consume current line comment
          while (Peek() != '\n' && !IsAtEnd()) Advance();
        } else {
          return;
        }
        break;
      default:return;
    }
  }
}

char Scanner::Peek() const {
  return *m_current;
}

char Scanner::PeekNext() {
  if (IsAtEnd()) return '\0';
  return *(m_current + 1);
}

Token Scanner::MakeString() {
  while (Peek() != '"' && !IsAtEnd()) {
    if (Peek() == '\n') m_line++;
    Advance();
  }

  if (IsAtEnd()) return ErrorToken("Unterminated string.");

  // The closing quote.
  Advance();
  return MakeToken(TOKEN_STRING);
}

Token Scanner::MakeNumber() {
  while (IsDigit(Peek())) Advance();

  // Look for a fractional part.
  if (Peek() == '.' && IsDigit(PeekNext())) {
    // Consume the ".".
    Advance();

    while (IsDigit(Peek())) Advance();
  }

  return MakeToken(TOKEN_NUMBER);
}
Token Scanner::MakeIdentifier() {
  while (IsAlpha(Peek()) || IsDigit(Peek())) Advance();

  return MakeToken(IdentifierType());
}

TokenType Scanner::IdentifierType() {
  switch (*m_start) {
    case 'a': return CheckKeyword(1, 2, "nd", TOKEN_AND);
    case 'c': return CheckKeyword(1, 4, "lass", TOKEN_CLASS);
    case 'e': return CheckKeyword(1, 3, "lse", TOKEN_ELSE);
    case 'f':
      if (m_current - m_start > 1) {
        switch (m_start[1]) {
          case 'a': return CheckKeyword(2, 3, "lse", TOKEN_FALSE);
          case 'o': return CheckKeyword(2, 1, "r", TOKEN_FOR);
          case 'u': return CheckKeyword(2, 1, "n", TOKEN_FUN);
        }
      }
      break;
    case 'i': return CheckKeyword(1, 1, "f", TOKEN_IF);
    case 'n': return CheckKeyword(1, 2, "il", TOKEN_NIL);
    case 'o': return CheckKeyword(1, 1, "r", TOKEN_OR);
    case 'p': return CheckKeyword(1, 4, "rint", TOKEN_PRINT);
    case 'r': return CheckKeyword(1, 5, "eturn", TOKEN_RETURN);
    case 's': return CheckKeyword(1, 4, "uper", TOKEN_SUPER);
    case 't':
      if (m_current - m_start > 1) {
        switch (m_start[1]) {
          case 'h': return CheckKeyword(2, 2, "is", TOKEN_THIS);
          case 'r': return CheckKeyword(2, 2, "ue", TOKEN_TRUE);
        }
      }
      break;
    case 'v': return CheckKeyword(1, 2, "ar", TOKEN_VAR);
    case 'w': return CheckKeyword(1, 4, "hile", TOKEN_WHILE);
  }
  return TOKEN_IDENTIFIER;
}

TokenType Scanner::CheckKeyword(int start, int length, const std::string &rest, TokenType type) const {
  if (m_current - m_start != start + length) return TOKEN_IDENTIFIER;
  std::string res_str{m_start + start, m_current};

  if (res_str == rest) {
    return type;
  }

  return TOKEN_IDENTIFIER;
}
}
